package domain.player.ai;

import domain.GameGrid;
import domain.player.Disc;

/**
 * Implementing MiniMax with alpha-beta cutoffs for a Player in a game of
 * connectFour. It is using AI_MMEval for evaluating a gamegrid/gamestate.
 */
public class MiniMax extends AI {

    public static final int DEFAULT_DEPTH = 7;
    /**
     * The depth of the search tree when checking for good moves
     */
    private static final int INFINITY = 200000000;
    /**
     * The winning constant. It's the same for both the max and min player and
     * used mostly in the state evaluation class
     */
    protected static final int WINNING = 190000000;
    /**
     * Default choice. If it's still at DEFAULT_CHOICE after checking all moves,
     * a random choice is made.
     */
    private static final int DEFAULT_CHOICE = -1;
    /**
     * The maximum amount of moves ahead it searches
     */
    private int ply = DEFAULT_DEPTH;
	private boolean debug = false;
	private boolean alphaBeta = true;
    /**
     * grid evaluation instance
     */
    private Evaluation ai;
    /**
     * so it searches the columns from the middle out
     */
    private static int[] columnPriority;
    private int nrOfTriedMoves = 0;
    private int moveScore = 0;

	int columnAB, column, triedAB = 0, pointsAB, points;

    /*
     * Constructor.
	 *
     */
    public MiniMax(Disc discID, boolean debug, boolean alphaBeta, int ply) {
        super(discID);

        ai = new Evaluation();

		this.debug = debug;
		this.alphaBeta = alphaBeta;
		this.ply = ply;

        columnPriority = new int[GameGrid.GRID_COLUMNS];
        initPriority();
    }

    public void setPly(int depth) {
        this.ply = depth;
    }

    public int getPly() {
        return ply;
    }

	public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Sets ai so it uses the grid that is provided and returns the best column
     * in the current game state.
     *
     * @param grid the current game state/GameGrid
     * @return the best column to choose it finds
     */
    @Override
    public int getGoodMove(GameGrid grid) {
		ai.setGameGrid(grid);
        nrOfTriedMoves = 0;
		//System.out.println(ply + " PLY, DEBUG: " + debug);
		getGoodColumnAlphaBeta();
//		if(debug){
//			nrOfTriedMoves = 0;
//			getGoodColumn();
//		}
        return columnAB;
    }

    /**
     * Tests all different moves you can make and checks which one gives you the
     * most points, assuming your opponent will make the best move possible (for
     * him) in every state.
     *
     * @return An integer representing the best column to pick.
     */
    private int getGoodColumnAlphaBeta() {
        int bestChoice = DEFAULT_CHOICE;
		int row;

        int bestPoints = -INFINITY,
                points = -INFINITY;

        int alpha = -INFINITY,
            beta = INFINITY;

		nrOfTriedMoves++;
        // tests all valid moves, saves the best one.
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

                row = ai.setDisc(columnPriority[i], getDiscID());
                if( ai.isWinningMove(row, columnPriority[i]) ){
                    bestChoice = columnPriority[i];
                    bestPoints = WINNING;
					break;
                }

				points = minAlphaBeta(1, otherDisc(getDiscID()), alpha, beta);

				ai.removeDisc(columnPriority[i]);

                if(points > bestPoints) {
                    bestChoice = columnPriority[i];
                    bestPoints = points;
				}
            }
            moveScore = bestPoints;
        }

		triedAB = nrOfTriedMoves;
		pointsAB = bestPoints;
		columnAB = bestChoice;

		System.out.println("(Alpha-Beta) Nodes visited: " + triedAB + ", points: " + pointsAB + ", column: " + columnAB);

		return bestChoice;
    }

	/**
     * MAX!
     */
    private int maxAlphaBeta(int depth, Disc disc, int alpha, int beta) {
        int points = -INFINITY;
        int maxPoints = -INFINITY;
		int row;

		nrOfTriedMoves++;

		if (ai.isTie()) return 0;
        if (depth > ply)
            return (ai.evalState(disc) - ai.evalState(otherDisc(disc)));

        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

                row = ai.setDisc(columnPriority[i], disc);
                if (ai.isWinningMove(row, columnPriority[i])) {
                    ai.removeDisc(columnPriority[i]);
                    return (WINNING - depth);
                }

                points = minAlphaBeta(depth + 1, otherDisc(disc), alpha, beta);
				if(points > maxPoints)
					maxPoints = points;

				ai.removeDisc(columnPriority[i]);

				if (points >= beta) return points;

				if (points > alpha) alpha = points;
            }
        }
        return maxPoints;
    }

	/**
     * min...
     */
    private int minAlphaBeta(int depth, Disc disc, int alpha, int beta) {
        int points = +INFINITY;
        int minPoints = +INFINITY;
		int row;

		nrOfTriedMoves++;

		if (ai.isTie()) return 0;
        if (depth > ply)
            return ai.evalState(otherDisc(disc)) - ai.evalState(disc);

        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

				row = ai.setDisc(columnPriority[i], disc);
                if (ai.isWinningMove(row, columnPriority[i])) {
                    ai.removeDisc(columnPriority[i]);
                    return (-WINNING + depth);
                }

                points = maxAlphaBeta(depth + 1, otherDisc(disc), alpha, beta);
				if (points < minPoints)
					minPoints = points;

                ai.removeDisc(columnPriority[i]);

                if (minPoints >= beta) return minPoints;

				if (points < beta) beta = points;
            }
        }
        return minPoints;
    }

	private int getGoodColumn() {
        int bestChoice = DEFAULT_CHOICE;
		int row;

        int bestPoints = -INFINITY,
                points = -INFINITY;

        int alpha = -INFINITY,
            beta = INFINITY;

		nrOfTriedMoves++;
        // tests all valid moves, saves the best one.
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

                row = ai.setDisc(columnPriority[i], getDiscID());
                if( ai.isWinningMove(row, columnPriority[i]) ){
                    bestChoice = columnPriority[i];
                    bestPoints = WINNING;
					break;
                }

				points = min(1, otherDisc(getDiscID()));

				ai.removeDisc(columnPriority[i]);

                if(points > bestPoints) {
                    bestChoice = columnPriority[i];
                    bestPoints = points;
				}
            }
            moveScore = bestPoints;
        }

		System.out.println("Nodes visited: " + nrOfTriedMoves + ", points: " + bestPoints + ", column: " + bestChoice);

		return bestChoice;
    }

	/**
     * MAX!
     */
    private int max(int depth, Disc disc) {
        int points = -INFINITY, point;
        int maxPoints = -INFINITY;
		int row;

		nrOfTriedMoves++;

		if (ai.isTie()) return 0;
        if (depth > ply){
            point =  (ai.evalState(disc) - ai.evalState(otherDisc(disc)));
			//System.out.println(point);
            return point;
		}
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

                row = ai.setDisc(columnPriority[i], disc);
                if (ai.isWinningMove(row, columnPriority[i])) {
                    ai.removeDisc(columnPriority[i]);
                    return (WINNING - depth);
                }

                points = min(depth + 1, otherDisc(disc));
				if(points > maxPoints)
					maxPoints = points;

				ai.removeDisc(columnPriority[i]);
            }
        }
        return maxPoints;
    }

	 private int min(int depth, Disc disc) {
        int points = +INFINITY, point;
        int minPoints = +INFINITY;
		int row;

		nrOfTriedMoves++;

		if (ai.isTie()) return 0;
        if (depth > ply){
			point = ai.evalState(otherDisc(disc)) - ai.evalState(disc);
			//System.out.println(point);
            return point;
		}
        for (int i = 0; i < GameGrid.GRID_COLUMNS; i++) {
            if (ai.isValidMove(columnPriority[i])) {

				row = ai.setDisc(columnPriority[i], disc);
                if (ai.isWinningMove(row, columnPriority[i])) {
                    ai.removeDisc(columnPriority[i]);
                    return (-WINNING + depth);
                }

                points = max(depth + 1, otherDisc(disc));
				if (points < minPoints)
					minPoints = points;

                ai.removeDisc(columnPriority[i]);
            }
        }
        return minPoints;
    }

    private Disc otherDisc(Disc discID) {
        return (discID == Disc.ONE) ? Disc.TWO : Disc.ONE;
    }

    @Override
    public int getType() {
        return AI.AI_MINIMAX;
    }

    public int getNrOfTriedMoves() {
        return nrOfTriedMoves;
    }

    /**
     * Initiates the columnPriority vector so that the middle column index comes
     * first, followed by the ones closests to the center and so on.
     *
     * That way the evaluation algorithm doesn't start by looking at the column
     * at a corner, since it should be more beneficial to place a disc in the
     * middle columns if you want to make good moves (in case all moves you can
     * make returns the same result).
     */
    private void initPriority() {
        int i = 0, j = 0,
                middleIndex = ((int) ((GameGrid.GRID_COLUMNS - 1) / 2));

        // if there's an uneven number of columns.
        if (GameGrid.GRID_COLUMNS % 2 == 1) {
            columnPriority[i] = middleIndex;
            i++;
            j++;
        }

        for (; i < GameGrid.GRID_COLUMNS; i++) {
            if (i % 2 == 0) {
                columnPriority[i] = middleIndex - j;
                j++;
            } else {
                columnPriority[i] = middleIndex + j;
            }
        }
    }

    public int getMoveScore(){
        return moveScore;
    }
}